﻿using System;
using System.Collections.Generic;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Windows.Input;
using System.Windows;

namespace Charting
{
    public class Chart : UserControl
    {
        Viewport3D viewport;
        Model3DGroup modelGroup;
        ScaleTransform3D scaleTransform;
        RotateTransform3D rotateTransform;
        BoundingPlanes boundingPlanes;
        Vector3D previousMouseVector;
        List<Tube> tubes = new List<Tube>();
        Tube hoverTube;

        public Chart()
        {
            this.MouseLeftButtonDown += this.ChartMouseLeftButtonDown;
            this.MouseLeftButtonUp += this.ChartMouseLeftButtonUp;
            this.MouseMove += this.ChartMouseMove;
            this.MouseWheel += this.ChartMouseWheel;

            this.viewport = new Viewport3D { ClipToBounds = true };
            this.Content = viewport;
          
            viewport.Camera = new PerspectiveCamera { Position = new Point3D(0, 0, 6) };

            var lightingVisual = new ModelVisual3D();
            viewport.Children.Add(lightingVisual);

            var lightingGroup = new Model3DGroup();
            lightingVisual.Content = lightingGroup;

            lightingGroup.Children.Add(
                new AmbientLight(Color.FromRgb(64, 64, 64)));
            lightingGroup.Children.Add(
                new DirectionalLight(Colors.White, new Vector3D(3, -1, -3)));

            var modelVisual = new ModelVisual3D();
            viewport.Children.Add(modelVisual);

            this.modelGroup = new Model3DGroup { Transform = this.CreateTransformGroup() };
            modelVisual.Content = this.modelGroup;
        }

        public void SetData(ChartData chartData)
        {
            this.boundingPlanes = new BoundingPlanes(chartData);
            this.modelGroup.Children.Add(this.boundingPlanes.Model);

            double rowSpacing = 2D / (chartData.Rows.Count + 1);
            double columnSpacing = 2D / (chartData.Columns.Count + 1);

            foreach (string row in chartData.Rows)
            {
                Color color = chartData.GetRowColor(row);
                double offsetZ = (chartData.Rows.IndexOf(row) + 1) * rowSpacing - 1;
                
                foreach (string column in chartData.Columns)
                {
                    string tip =
                        chartData.GetValue(row, column).ToString(chartData.RangeFormatString);

                    double offsetX = (chartData.Columns.IndexOf(column) + 1) * columnSpacing - 1;
                    double height = 2 * chartData.GetValue(row, column) / chartData.Range;

                    var tube = new Tube(new Vector3D(offsetX, -1, offsetZ), height, color, tip);
                    this.tubes.Add(tube);
                    this.modelGroup.Children.Add(tube.Model);
                }
            }
        }

        void ChartMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            Point currentMousePoint = e.GetPosition(this);
            this.previousMouseVector = this.ProjectToTrackball(currentMousePoint);

            this.CaptureMouse();
        }

        void ChartMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (this.IsMouseCaptured) this.ReleaseMouseCapture();
        }

        void ChartMouseMove(object sender, MouseEventArgs e)
        {
            Point currentMousePoint = e.GetPosition(this);

            if (this.IsMouseCaptured)
            {
                var currentMouseVector3D = this.ProjectToTrackball(currentMousePoint);

                Vector3D changeAxis = Vector3D.CrossProduct(
                    this.previousMouseVector, currentMouseVector3D);
                double changeAngle = Vector3D.AngleBetween(
                    this.previousMouseVector, currentMouseVector3D);

                if (changeAxis.LengthSquared > 0)
                {
                    var rotation = this.rotateTransform.Rotation as AxisAngleRotation3D;

                    if (rotation != null)
                    {
                        Quaternion quaternion = new Quaternion(changeAxis, changeAngle) *
                            new Quaternion(rotation.Axis, rotation.Angle);

                        rotation.Axis = quaternion.Axis;
                        rotation.Angle = Math.Max(-40, Math.Min(40, quaternion.Angle));
                    }
                }

                this.previousMouseVector = currentMouseVector3D;
            }

            this.HoverHitTest(currentMousePoint);
        }

        void ChartMouseWheel(object sender, MouseWheelEventArgs e)
        {
            double multiplier = Math.Exp((double)e.Delta / -1000);
            double scale = Math.Max(0.4, Math.Min(4,
                this.scaleTransform.ScaleX * multiplier));

            this.scaleTransform.ScaleX = scale;
            this.scaleTransform.ScaleY = scale;
            this.scaleTransform.ScaleZ = scale;

            this.HoverHitTest(e.GetPosition(this));

            e.Handled = true;
        }

        void HoverHitTest(Point currentMousePoint)
        {
            HitTestResult hitTestResult =
                VisualTreeHelper.HitTest(this.viewport, currentMousePoint);
            RayMeshGeometry3DHitTestResult rayHitTestResult =
                hitTestResult as RayMeshGeometry3DHitTestResult;

            Tube newHoverTube = null;

            if (rayHitTestResult != null)
            {
                foreach (Tube tube in this.tubes)
                {
                    if (rayHitTestResult.ModelHit == tube.Model)
                    {
                        newHoverTube = tube;
                        break;
                    }
                }
            }

            if (newHoverTube != this.hoverTube)
            {
                if (this.hoverTube != null) this.hoverTube.IsHovered = false;

                this.hoverTube = newHoverTube;

                this.boundingPlanes.Tip = this.hoverTube != null ? this.hoverTube.Tip : "";

                if (this.hoverTube != null) this.hoverTube.IsHovered = true;
            }
        }

        Transform3DGroup CreateTransformGroup()
        {
            var transformGroup = new Transform3DGroup();

            var rotateTransform1 = new RotateTransform3D(
                new AxisAngleRotation3D(new Vector3D(0, 1, 0), -45));
            transformGroup.Children.Add(rotateTransform1);

            var rotateTransform2 = new RotateTransform3D(
                new AxisAngleRotation3D(new Vector3D(1, 0, 0), 35));
            transformGroup.Children.Add(rotateTransform2);

            var translateTransform = new TranslateTransform3D(0, 0.2, 0);
            transformGroup.Children.Add(translateTransform);

            this.scaleTransform = new ScaleTransform3D(1, 1, 1);
            transformGroup.Children.Add(this.scaleTransform);

            this.rotateTransform = new RotateTransform3D(new AxisAngleRotation3D());
            transformGroup.Children.Add(this.rotateTransform);

            return transformGroup;
        }

        Vector3D ProjectToTrackball(Point point)
        {
            double x = 2 * point.X / this.ActualWidth - 1;
            double y = 1 - 2 * point.Y / this.ActualHeight;

            double zSquared = 1 - x * x - y * y;
            double z = zSquared > 0 ? Math.Sqrt(zSquared) : 0;

            return new Vector3D(x, y, z);
        }
    }
}
